(function (fruit, util) {
    var identity = 1, metaPrefix = '@', eventPrefix = 'on', classMap = {};

    var internals = {
        addListener: function (owner, handler, context) {
            var map = this.__listeners__;
            var id = owner.$id;
            var listeners = map[id] = map[id] || [
                {owner: null, handler: null, context: null}
            ];
            listeners.push({
                owner: owner,
                handler: handler,
                context: context || owner
            });
        },
        removeListener: function (owner, handler, context) {
            var listeners = this.__listeners__[owner.$id], listener;
            if (listeners) {
                for (var i = 0, length = listeners.length; i < length; i++) {
                    listener = listeners[i];
                    if (listener.handler == handler && listener.context == context) {
                        listeners.splice(i, 1);
                        break;
                    }
                }
            }
        },
        clearListeners: function (owner) {
            var listeners = this.__listeners__[owner.$id];
            if (listeners) {
                listeners.length = 0;
            }
        },
        notifyListeners: function (owner, data) {
            var listeners = this.__listeners__[owner.$id], listener, result;
            if (listeners) {
                for (var i = 0, length = listeners.length; i < length; i++) {
                    listener = listeners[i];
                    if (listener && listener.handler) {
                        result = listener.handler.call(listener.context, listener.owner, data);
                        if (result === false) {
                            return false;
                        }
                    }
                }
            }
        },
        propertyGetter: function (prop) {
            return this['_' + prop.propertyName];
        },
        propertySetter: function (value, prop) {
            this['_' + prop.propertyName] = value;
        },
        staticHas: function (name) {
            var member = this.prototype[name];
            return !!member && member.type == 'property';
        },
        staticCan: function (name) {
            var member = this.prototype[name];
            return !!member && member.type == 'event';
        },
        staticCreate: function (method, args) {
            var prototype = this.prototype, result, fn;
            prototype.initializer = null;
            result = new this();
            fn = result[method];
            if (fn) {
                fn.apply(result, args);
            }
            prototype.initializer = prototype.init;
            return result;
        },
        emptyFunction: function () {
        }
    }

    function defineEvent(name, config) {
        var Event = function (handler, context) {
            var map = Event.__listeners__;
            var id = this.$id;
            var listeners = map[id] = map[id] || [
                {owner: null, handler: null, context: null}
            ];

            listeners[0] = {
                owner: this,
                handler: handler,
                context: context
            }
        };

        Event.__listeners__ = {};
        Event.name = name;
        Event.type = 'event';
        Event.config = config;
        Event.addListener = internals.addListener;
        Event.removeListener = internals.removeListener;
        Event.clearListeners = internals.clearListeners;
        Event.notifyListeners = internals.notifyListeners;

        return Event;
    }

    function defineProperty(name, config) {
        var Property;

        if (config.readonly) {
            Property = function (value, force) {
                if (value === undefined) {
                    return Property.getter.call(this, Property);
                }
                else {
                    if (this.__initializing__ || force) {
                        return Property.setter.call(this, value, Property);
                    }
                    else {
                        return false;
                    }
                }
            };
        }
        else if (config.observable) {
            Property = function (value, silent) {
                var oldValue = Property.getter.call(this, Property);

                if (value === undefined) {
                    return oldValue;
                }
                else {
                    if (oldValue !== value) {
                        if (Property.setter.call(this, value, Property) !== false && !silent) {
                            var data = {
                                property: name,
                                oldValue: oldValue,
                                newValue: value
                            };

                            Property.notifyListeners(this, data);
                            this.fire('propertyChanged', data);
                            return true;
                        }
                        else {
                            return false;
                        }
                    }
                    else {
                        return false;
                    }
                }
            };

            Property.__listeners__ = {};
            Property.addListener = internals.addListener;
            Property.removeListener = internals.removeListener;
            Property.clearListeners = internals.clearListeners;
            Property.notifyListeners = internals.notifyListeners;
        }
        else {
            Property = function (value) {
                if (value === undefined) {
                    return Property.getter.call(this, Property);
                }
                else {
                    return Property.setter.call(this, value, Property);
                }
            };
        }

        Property.propertyName = name;
        Property.type = 'property';
        Property.config = config;
        Property.getter = config.getter === undefined ? internals.propertyGetter : config.getter;
        Property.setter = config.setter === undefined ? internals.propertySetter : config.setter;
        Property.setter = Property.setter || internals.emptyFunction;

        if (config.type) {
            var oldSetter = Property.setter;
            var dataType = config.type;
            Property.setter = function (value, data) {
                oldSetter.call(this, dataType.$type ? new dataType(value) : dataType(value), data);
            }
        }

        Property.defaultValue = config.value;
        Property.readonly = !!config.readonly;
        Property.observable = !!config.observable;

        //TODO: property meta should be stored as is
        Property.bindingMode = config.bindingMode;
        Property.serializable = config.serializable;

        return Property;
    }

    /**
     * RootClass is the base of any Classes defined in this framework
     * @constructor
     */
    function RootClass() {
    }

    RootClass.prototype = {
        constructor: RootClass,
        /**
         * Call overridden method from super class
         * @returns {*}
         */
        inherited: function () {
            var base = this.inherited.caller._super;
            if (base) {
                return base.apply(this, arguments);
            }
        },
        /**
         * Check whether target object equals self.
         * @param target
         * @returns {boolean}
         */
        equals: function (target) {
            return this === target;
        },
        /**
         * Check whether current object is specified type.
         * @param type
         * @returns {boolean}
         */
        is: function (type) {
            return this instanceof type;
        },
        /**
         * Check whether current object has specified property.
         * @param name
         * @returns {*|boolean}
         */
        has: function (name) {
            var member = this[name];
            return member && member.type == 'property';
        },
        /**
         * Get specified property value.
         * @param name
         * @returns {*}
         */
        get: function (name) {
            var member = this[name];
            if (member !== undefined) {
                if (member.type == 'property') {
                    return member.call(this);
                }
                else {
                    return member;
                }
            }
        },
        /**
         * Set specified property value.
         * @param name
         * @param value
         * @param force
         * @returns {*}
         */
        set: function (name, value, force) {
            var member = this[name];
            if (member !== undefined) {
                if (member.type == 'property') {
                    return member.call(this, value, force);
                }
                else {
                    this[name] = value;
                    return true;
                }
            }
            else {
                if (force) {
                    this[name] = value;
                    return true;
                }
                else {
                    return false;
                }
            }
        },
        /**
         * Get property value using a path.
         * @param path
         * @returns {*}
         */
        getByPath: function (path) {
            if (path) {
                var index = path.indexOf('.');
                var context, subPath;

                if (index >= 0) {
                    context = this.get(path.slice(0, index));
                    subPath = path.slice(index + 1);
                    if (context) {
                        return context.getByPath(subPath);
                    }
                }
                else {
                    return this.get(path);
                }
            }
            else {
                return this;
            }
        },
        /**
         * Set property value using a path.
         * @param path
         * @param value
         * @returns {*}
         */
        setByPath: function (path, value) {
            if (path) {
                var index = path.indexOf('.');
                var context, subPath;

                if (index >= 0) {
                    context = this.get(path.slice(0, index));
                    subPath = path.slice(index + 1);
                    if (context) {
                        return context.setByPath(subPath, value);
                    }
                }
                else {
                    return this.set(path, value);
                }
            }
        },
        /**
         * Get all property values.
         * @returns {{}}
         */
        getAll: function () {
            var names = this.__properties__, name;
            var i = 0, length = names.length;
            var props = {};
            for (; i < length; i++) {
                name = names[i];
                props[name] = this.get(name);
            }

            return props;
        },
        /**
         * Set all property values.
         * @param props
         */
        setAll: function (props) {
            if (props) {
                var name, value;
                for (name in props) {
                    value = props[name];
                    this.setByPath(name, value);
                }
            }
        },
        /**
         * Check whether current object has specified event.
         * @param name
         * @returns {boolean}
         */
        can: function (name) {
            return !!this[eventPrefix + name];
        },
        /**
         * Add an event handler.
         * @param name
         * @param handler
         * @param context
         */
        on: function (name, handler, context) {
            var event = this[eventPrefix + name];
            if (event) {
                event.addListener(this, handler, context);
            }
        },
        /**
         * Remove an event handler.
         * @param name
         * @param handler
         * @param context
         */
        off: function (name, handler, context) {
            var event = this[eventPrefix + name];
            if (event) {
                if (handler === undefined) {
                    event.clearListeners(this);
                }
                else {
                    event.removeListener(this, handler, context);
                }
            }
        },
        /**
         * Add a single event handler.
         * @param name
         * @param handler
         * @param context
         */
        one: function (name, handler, context) {
            var event = this[eventPrefix + name];
            if (event) {
                event.call(this, handler, context);
            }
        },
        /**
         * Trigger an event.
         * @param name
         * @param data
         * @returns {*}
         */
        fire: function (name, data) {
            var event = this[eventPrefix + name];
            if (event) {
                return event.notifyListeners(this, data);
            }
        },
        /**
         * Destroy current object.
         */
        destroy: function () {
            var events = this.constructor.__events__, event;
            for (var i = 0, length = events.length; i < length; i++) {
                event = this[eventPrefix + events[i]];
                if (event) {
                    event.clearListeners(this);
                }
            }
        }
    };

    function extendEvent(cls, name, config) {
        var prototype = cls.prototype;
        prototype[eventPrefix + name] = defineEvent(name, config);
    }

    function extendProperty(cls, name, config) {
        var prototype = cls.prototype;
        if (config.inherits) {
            var oldProperty = prototype[name];
            if (oldProperty) {
                config = util.extend({}, oldProperty.config, config);
            }
        }

        var property = prototype[name] = defineProperty(name, config);
        prototype['_' + name] = property.defaultValue;
    }

    function extendMethod(cls, name, method) {
        var prototype = cls.prototype;
        if (prototype[name]) {
            method._super = prototype[name];
        }
        prototype[name] = method;
    }

    function define(t, p, m) {
        var type = null, parents = null, members = null;
        if (typeof(t) === 'string') {
            type = t;
            parents = p;
            members = m;
        }
        else {
            parents = t;
            members = p;
        }
        if (parents) {
            if (parents instanceof Function) {
                parents = [parents];
            }
            else if (!(parents instanceof Array)) {
                members = parents;
                parents = [];
            }
        } else {
            parents = [];
        }

        members = members || {};

        var sup = parents[0] || RootClass;
        var meta = members.meta || {};
        var events = members.events || [];
        var props = members.properties || {};
        var methods = members.methods || {};
        var singleton = members.singleton;
        var statics = members.statics || {};
        var prototype, result;
        var key, value, length;

        function Class() {
            this.$id = identity++;
            if (this.initializer) {
                this.__initializing__ = true;
                this.initializer.apply(this, arguments);
                this.__initializing__ = false;
            }
        }

        function SuperClass() {
        }

        SuperClass.prototype = sup.prototype;
        prototype = new SuperClass();
        prototype.constructor = Class;
        Class.$id = identity++;
        Class.$super = prototype.$super = sup;
        Class.$type = prototype.$type = type ? type : 'Anonymous';
        Class.prototype = prototype;
        Class.has = internals.staticHas;
        Class.can = internals.staticCan;
        Class.create = internals.staticCreate;

        Class.__events__ = prototype.__event__ = util.unique(events.slice().concat(sup.__events__ || []));
        Class.__properties__ = prototype.__properties__ = util.unique(util.keys(props).concat(sup.__properties__ || []));
        Class.__methods__ = prototype.__methods__ = util.unique(util.keys(methods).concat(sup.__methods__ || []));

        if (methods.init) {
            prototype.initializer = methods.init;
        }

        for (key in meta) {
            value = meta[key];
            prototype[metaPrefix + key] = value;
        }

        if (events instanceof Array) {
            for (key = 0, length = events.length; key < length; key++) {
                value = events[key];
                extendEvent(Class, value);
            }
        }
        else {
            for (key in events) {
                value = events[key];
                extendEvent(Class, key, value);
            }
        }

        for (key in props) {
            value = props[key];
            extendProperty(Class, key, value);
        }

        for (key in methods) {
            value = methods[key];
            extendMethod(Class, key, value);
        }

        for (key in statics) {
            value = statics[key];
            Class[key] = value;
        }

        if (singleton) {
            result = new Class();
        }
        else {
            result = Class;
        }

        if (type) {
            fruit.util.setByPath(type, result);
        }

        classMap[result.$id] = result;

        return result;
    }

    fruit.define = define;
    fruit.Object = RootClass;
    fruit.getClassById = function (id) {
        return classMap[id];
    };

    if (fruit.DEBUG) {
        fruit.classMap = classMap;
    }

})(fruit, fruit.util);
